-module(rtps_participant).

-behaviour(gen_statem).

%% -include_lib("kernel/include/logger.hrl").
-include("rtps.hrl").
-include("rtps_spdp.hrl").
-include("rtps_sedp.hrl").
-include("rtps_psm.hrl").

%% API
-export([start_link/4, add_endpoint/4, del_endpoint/2, whereis/2, info/1]).

%% gen_statem callbacks
-export([init/1, callback_mode/0,
	 running/3,
	 handle_event/4, 
	 terminate/3, code_change/4]).

-record(data, {sup :: pid(), opts :: [proplists:property()],
	       guid_prefix :: rtps:guid_prefix(), participant_id :: rtps:participant_id(),
	       lease_duration :: rtps:duration(),
	       reg :: pid() | undefined, endpoints_sup :: pid() | undefined,
	       spdp :: pid() | undefined, sedp :: pid() | undefined}).

%%%===================================================================
%%% API
%%%===================================================================

start_link(Sup, Guid_prefix, Reg, Opts) ->
    gen_statem:start_link({via, rtps_reg, {Reg, Guid_prefix}}, ?MODULE, [Sup, Guid_prefix, Opts], []).

-spec add_endpoint(Pid, Kind, Entity_id, Opts) -> Result when
      Kind :: writer | reader,
      Pid :: pid(),
      Entity_id :: rtps:entity_id(),
      Opts :: [proplists:property()],
      Result :: supervisor:startchild_ret() | {error, Reason},
      Reason :: term(). 
%% @see add_endpoint
add_endpoint(Pid, Kind, Entity_id, Opts) ->
    gen_statem:call(Pid, {add_endpoint, Kind, Entity_id, Opts}).
   
-spec del_endpoint(Pid, Entity_id) -> ok when
      Pid :: pid(),
      Entity_id :: rtps:entity_id().
%% @doc Delete an endpoint from a participant.
del_endpoint(Pid, Entity_id) ->
    gen_statem:cast(Pid, {del_endpoint, Entity_id}).

-spec whereis(Pid, Entity_id) -> pid() | undefined when
      Pid :: pid(),
      Entity_id :: rtps:entity_id().
%% @doc This function returns the pid() of the endpoint, a writer or
%% reader, identified by the Entity_key, or undefined if no such
%% endpoint exists.
whereis(Pid, Entity_id) ->
    gen_statem:call(Pid, {whereis, Entity_id}).

info(Pid) ->
    gen_statem:call(Pid, info).

%%%===================================================================
%%% gen_statem callbacks
%%%===================================================================

%% @private
init([Sup, Guid_prefix, Opts]) ->
    Lease_duration = proplists:get_value(participant_lease_duration, Opts, ?PARTICIPANT_LEASE_DURATION),
    case proplists:get_value(builtin_endpoints, Opts) of
	Builtin_endpoints when Builtin_endpoints =:= undefined; Builtin_endpoints =:= [] ->
	    {stop, {badopt, builtin_endpoints}};
	_ ->
	    Participant_id = proplists:get_value(participant_id, Opts),
	    {ok, running, #data{sup = Sup,  opts = Opts, guid_prefix = Guid_prefix,
				participant_id = Participant_id,
				lease_duration = Lease_duration}}
    end.

%% @private
callback_mode() ->
    [state_functions, state_enter].

%% @private
running(enter, _Old_state, #data{sup = Sup, guid_prefix = Guid_prefix, opts = Opts,
				 lease_duration = Lease_duration} = Data) ->
    %% The supervisor is of type one-for-all, so we can assume the pids constants.
    Children = supervisor:which_children(Sup),
    {ok, Reg} = child(Children, registry),
    {ok, Endpoints_sup} = child(Children, endpoints),
    {ok, Spdp} = child(Children, spdp),
    {ok, Sedp} = child(Children, sedp),
    Participant_data = participant_data(Guid_prefix, Opts, Lease_duration),
    rtps_spdp:setup(Spdp, Participant_data),
    {keep_state, Data#data{reg = Reg, endpoints_sup = Endpoints_sup, spdp = Spdp, sedp = Sedp}};
%% Add an endpoint to the participant
running({call, From}, {add_endpoint, Kind, Entity_id, Extra_opts},
	#data{reg = Reg, endpoints_sup = Endpoints_sup, sedp = Sedp, opts = Opts} = _Data) ->
    Domain_id = proplists:get_value(domain_id, Opts),
    Default_mc_loc = rtps_udp:multicast_locator(Domain_id, Entity_id, Opts),
    M_locs = proplists:get_value(multicast_locator_list, Opts, [Default_mc_loc]),
    Opts1 = [{multicast_locator_list, M_locs}, {sedp, Sedp} | proplists:delete(multicast_locator_list, Opts)],
    Spec = #{id => Entity_id,
	     start => {rtps_endpoint_sup, start_link, [Kind, Entity_id, Reg, Extra_opts ++ Opts1]},
	     restart => permanent,
	     type => supervisor},
    Reply = rtps_ofo_sup:start_child(Endpoints_sup, Spec),
    {keep_state_and_data, [{reply, From, Reply}]};
%% Delete an endpoint
running(cast, {del_endpoint, Entity_id}, #data{endpoints_sup = Endpoints_sup}) ->
    rtps_ofo_sup:stop_child(Endpoints_sup, Entity_id),
    keep_state_and_data;
running({call, From}, {whereis, Entity_id}, #data{reg = Reg}) ->
    Reply = rtps_reg:whereis_name(Reg, Entity_id),
    {keep_state_and_data, [{reply, From, Reply}]};
running(Event_type, Event_content, Data) ->
    handle_event(Event_type, Event_content, running, Data).

%% @private
handle_event({call, From}, info, _State, 
	     #data{reg = Reg, spdp = Spdp, guid_prefix = Guid_prefix, participant_id = Participant_id}) ->
    Endpoints = rtps_reg:which(Reg),
    Reply = [{guid_prefix, Guid_prefix}, {participant_id, Participant_id},
	     {endpoints, [Entity_id || {Entity_id, _Pid} <- Endpoints]},
	     {remotes, rtps_spdp:info(Spdp)}],
    {keep_state_and_data, [{reply, From, Reply}]};

handle_event(enter,  _Old_state, _New_state, _Data) ->
    keep_state_and_data;

%% TODO: Sink unhadled events. Remove these clauses.
handle_event({call, From} = _Event_type, _Event_content, _State, _Data) ->
    %% ?LOG_DEBUG("Unhandled event: Type ~p, Content ~p", [Event_type,  Event_content]),
    Reply = ok,
    {keep_state_and_data, [{reply, From, Reply}]};
handle_event(_Event_type,  _Event_content, _State, _Data) ->
    %% ?LOG_DEBUG("Unhandled event: Type ~p, Content ~p", [Event_type,  Event_content]),
    keep_state_and_data.

%% @private
terminate(_Reason, _State, _Data) ->
    ok.

%% @private
code_change(_Old_vsn, State, Data, _Extra) ->
    {ok, State, Data}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

%% @private
child(Children, Id) ->
    case lists:keyfind(Id, 1, Children) of
	{_Id, Child, _Type, _Modules} when is_pid(Child) ->
	    {ok, Child};
	{_Id, Child, _Type, _Modules} when Child =:= restarting ->
	    {error, Child};
	_ ->
	    {error, notfound}
    end.

participant_data(Guid_prefix, Opts, Lease_duration) ->
    %% TODO: Do not calculate locators but use the actual values in use.
    Domain_id = proplists:get_value(domain_id, Opts),
    Expects_inline_qos = proplists:get_value(expects_inline_qos, Opts, false),
    %% Metatraffic
    Meta_u_locs = proplists:get_value(uniicast_locator_list, Opts, []),
    Meta_mc_loc = rtps_udp:multicast_locator(Domain_id, 'BuiltinParticipantMessageReader', Opts),
    Meta_m_locs = proplists:get_value(multicast_locator_list, Opts, [Meta_mc_loc]),
    %% default
    U_locs = proplists:get_value(uniicast_locator_list, Opts, []),
    Default_mc_loc = rtps_udp:multicast_locator(Domain_id, unknown, Opts),
    M_locs = proplists:get_value(multicast_locator_list, Opts, [Default_mc_loc]),

    Builtin_endpoints = proplists:get_value(builtin_endpoints, Opts),
    Manual_liveliness_count = 0,
    Proxy = #participant_proxy{protocol_version = ?VERSION,
			       guid_prefix = Guid_prefix,
			       vendor_id = ?VENDOR_ID,
			       expects_inline_qos = Expects_inline_qos,
			       metatraffic_unicast_locator_list = Meta_u_locs,
			       metatraffic_multicast_locator_list = Meta_m_locs,
			       default_unicast_locator_list = U_locs,
			       default_multicast_locator_list = M_locs,
			       available_builtin_endpoints = Builtin_endpoints,
			       manual_liveliness_count = Manual_liveliness_count},
    %% TODO: where do we get the QoS values from? (see Figure 8.27 -
    %% SPDPdiscoveredParticipantData p126, ParticipantBuiltinTopicData
    %% -key and -user_data
    #discovered_participant_data{participant_proxy = Proxy,
				 participant_builtin_topic_data = [],
				 lease_duration = Lease_duration}.
    
